package com.xored.searchwt.highlight;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * @author Yuri Strot
 *
 */
public class SearchEngine extends Thread {
	
	private Pattern pattern;
	private EditorAnnotationManager manager;
	private List matches;
	private Object monitor = new Object();
	private boolean exit;
	private boolean paused;
	private boolean prefix;
	
	private static final long PAUSE = 500;
	
	public void setMatcher(Pattern pattern, boolean prefix, EditorAnnotationManager manager, boolean paused) {
		synchronized (monitor) {
			this.pattern = pattern;
			this.manager = manager;
			this.paused = paused;
			this.prefix = prefix;
			if (pattern != null)
				interrupt();
		}
	}
	
	public void exit() {
		synchronized (monitor) {
			exit = true;
			interrupt();
		}
	}
	
	private boolean isMatcherChanged() {
		synchronized (monitor) {
			return pattern != null;
		}
	}
	
	private void sleepWhileInterrupted(long millis) {
		try {
			if (millis == 0) {
				while(true)
					Thread.sleep(50);
			}
			else
				Thread.sleep(millis);
		} catch (InterruptedException e1) {
		}
	}
	
	public void run() {
		while(!exit) {
			sleepWhileInterrupted(0);
			if (exit) break;
			EditorAnnotationManager manager = null;
			while(true) {
				long start = System.currentTimeMillis();
				if (!findMatches())
					continue;
				manager = this.manager;
				long waitPause = paused ? PAUSE - (
						System.currentTimeMillis() - start) : 0;
				if (waitPause > 0)
					sleepWhileInterrupted(waitPause);
				if (!isMatcherChanged())
					break;
			}
			Match[] result = (Match[])matches.toArray(new Match[matches.size()]);
			manager.highlight(result);
		}
	}
	
	private boolean findMatches() {
		DocumentCharSequence input = new DocumentCharSequence(manager.getDocument());
		matches = new ArrayList();
		boolean isEmpty = pattern == null || pattern.pattern().length() == 0;
		Matcher matcher = isEmpty ? null : pattern.matcher(new String());
		setMatcher(null, prefix, manager, paused);
		if (matcher == null)
			return true;
		matcher.reset(input);
		int k= 0;
		while (matcher.find()) {
			int start= matcher.start();
			if (!Character.isLetterOrDigit(input.charAt(start)) && prefix)
				start++;
			int end= matcher.end();
			if (end != start) { // don't report 0-length matches
				matches.add(new Match(start, end - start));
			}
			if (k++ == 20) {
				if (isMatcherChanged())
					return false;
				k = 0;
			}
		}
		return true;
	}
	
	public static Pattern createPattern(String pattern, boolean isCaseSensitive, boolean isPrefix) throws PatternSyntaxException {
		return createPattern(pattern, false, true, isCaseSensitive, isPrefix);
	}

	/**
	 * Creates a pattern element from the pattern string which is either a reg-ex expression or in our old
	 * 'StringMatcher' format.
	 * @param pattern The search pattern
	 * @param isRegex <code>true</code> if the passed string already is a reg-ex pattern
	 * @param isStringMatcher <code>true</code> if the passed string is in the StringMatcher format.
	 * @param isCaseSensitive Set to <code>true</code> to create a case insensitive pattern
	 * @param isPrefix <code>true</code> to create a pattern that requires a word boundary at the beginning and the end.
	 * @return The created pattern
	 * @throws PatternSyntaxException
	 */
	public static Pattern createPattern(String pattern, boolean isRegex, boolean isStringMatcher, boolean isCaseSensitive, boolean isPrefix) throws PatternSyntaxException {
		if (isRegex) {
			if (isPrefix) {
				StringBuffer buffer= new StringBuffer(pattern.length() + 10);
				buffer.append("\\b(?:").append(pattern).append(")\\b"); //$NON-NLS-1$ //$NON-NLS-2$
				pattern= buffer.toString();
			}
		} else {
			int len= pattern.length();
			StringBuffer buffer= new StringBuffer(len + 10);
			if (len > 0 && isPrefix) {
				buffer.append("(\\A|\\W)");
			}
			appendAsRegEx(isStringMatcher, pattern, buffer);
			pattern= buffer.toString();
		}

		int regexOptions= Pattern.MULTILINE;
		if (!isCaseSensitive) {
			regexOptions|= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
		}
		return Pattern.compile(pattern, regexOptions);
	}
	
	public static StringBuffer appendAsRegEx(boolean isStringMatcher, String pattern, StringBuffer buffer) {
        boolean isEscaped= false;
        for (int i = 0; i < pattern.length(); i++) {
            char c = pattern.charAt(i);
            switch(c) {
            // the backslash
            case '\\':
                // the backslash is escape char in string matcher
                if (isStringMatcher && !isEscaped) {
                    isEscaped= true;
                }
                else {
                    buffer.append("\\\\");  //$NON-NLS-1$
                    isEscaped= false;
                }
                break;
            // characters that need to be escaped in the regex.
            case '(':
            case ')':
            case '{':
            case '}':
            case '.':
            case '[':
            case ']':
            case '$':
            case '^':
            case '+':
            case '|':
                if (isEscaped) {
                    buffer.append("\\\\");  //$NON-NLS-1$
                    isEscaped= false;
                }
                buffer.append('\\');
                buffer.append(c);
                break;
            case '?':
                if (isStringMatcher && !isEscaped) {
                    buffer.append('.');
                }
                else {
                    buffer.append('\\');
                    buffer.append(c);
                    isEscaped= false;
                }
                break;
            case '*':
                if (isStringMatcher && !isEscaped) {
                    buffer.append(".*"); //$NON-NLS-1$
                }
                else {
                    buffer.append('\\');
                    buffer.append(c);
                    isEscaped= false;
                }
                break;
            default:
                if (isEscaped) {
                    buffer.append("\\\\");  //$NON-NLS-1$
                    isEscaped= false;
                }
                buffer.append(c);
                break;
            }
        }
        if (isEscaped) {
            buffer.append("\\\\");  //$NON-NLS-1$
            isEscaped= false;
        }
        return buffer;
    }

}
